﻿Imports System.Threading
Imports System.Runtime.InteropServices

Public Class ImapPusherServiceManager

    ''' <summary>
    ''' Get the available destinations from the connection enumerator.
    ''' </summary>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Function GetAvailableDestinations() As Generic.List(Of String)
        Dim _dests As Generic.List(Of String) = ImapPusher.ConnectionsEnumerator.GetAvailableConnections
        Return _dests
    End Function

    ''' <summary>
    ''' Gather settings from the user and save them to the settings xml file.
    ''' </summary>
    ''' <param name="pMailboxName"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Function GatherSettings(ByVal pMailboxName As String) As ImapPusher.AppSettings
        Dim _asc As New ImapPusher.AppSettingsCollection
        m_TraceFileLocation = _asc.TraceFileLocation
        Dim _f As New Settings
        Dim _as As New ImapPusher.AppSettings
        _f.cbDestinations.Items.Add("Auto")
        For Each _dest As String In GetAvailableDestinations()
            _f.cbDestinations.Items.Add(_dest)
        Next
        If pMailboxName IsNot Nothing Then
            If _asc.Settings.ContainsKey(pMailboxName) Then
                Dim _ps As ImapPusher.AppSettings = _asc.Settings(pMailboxName)
                _f.tbServer.Text = _ps.Server
                _f.nudPort.Value = _ps.Port
                _f.tbUserName.Text = _ps.Username
                _f.tbPassword.Text = _ps.Password
                _f.tbBoxName.Text = _ps.MailboxName
                _f.TraceFileLoc = _ps.TraceFileLocation
                _f.nudRefresh.Value = _ps.Refresh
                If _ps.SpecificConnection <> -1 Then
                    _f.cbDestinations.SelectedIndex = _ps.SpecificConnection
                End If
                If Not String.IsNullOrEmpty(_ps.Password) Then
                    _f.chkSaveSettings.Checked = True
                End If
            End If
        End If

        'Show the form to gather the application settings.
        _f.ShowDialog()
        _as.Server = _f.tbServer.Text
        _as.Port = CInt(_f.nudPort.Value)
        _as.Username = _f.tbUserName.Text
        If _f.chkSaveSettings.Checked Then
            _as.Password = _f.tbPassword.Text
        Else
            _as.Password = ""
        End If
        _as.MailboxName = _f.tbBoxName.Text
        _as.TraceFileLocation = _f.TraceFileLoc
        _as.Refresh = CInt(_f.nudRefresh.Value)

        Dim _destIndex As Integer
        Dim _UseSpecific As Boolean = False

        If _f.cbDestinations.SelectedIndex <> 0 And _f.cbDestinations.SelectedIndex <> -1 Then
            _destIndex = _f.cbDestinations.SelectedIndex - 1
            _UseSpecific = True
        End If

        If _UseSpecific Then
            _as.SpecificConnection = _destIndex
        Else
            _as.SpecificConnection = -1
        End If

        If _asc.Settings.ContainsKey(_as.MailboxName) Then
            _asc.Settings.Remove(_as.MailboxName)
        End If
        _asc.Settings.Add(_as.MailboxName, _as)
        _asc.TraceFileLocation = m_TraceFileLocation
        _asc.Save()

        Return _as
    End Function

    ''' <summary>
    ''' Create a new mailbox item.
    ''' </summary>
    ''' <param name="sender"></param>
    ''' <param name="e"></param>
    ''' <remarks></remarks>
    Private Sub btnNew_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnNew.Click
        Dim _as As ImapPusher.AppSettings = GatherSettings(Nothing)
        lstMailboxes.Items.Add(_as.MailboxName)
    End Sub

    ''' <summary>
    ''' Edit an existing mailbox item.
    ''' </summary>
    ''' <param name="sender"></param>
    ''' <param name="e"></param>
    ''' <remarks></remarks>
    Private Sub btnEdit_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnEdit.Click
        If lstMailboxes.SelectedIndex < 0 Then
            Exit Sub
        End If
        Dim _mbName As String = lstMailboxes.Text
        GatherSettings(_mbName)
    End Sub

    ''' <summary>
    ''' delete an existing mailbox item.
    ''' </summary>
    ''' <param name="sender"></param>
    ''' <param name="e"></param>
    ''' <remarks></remarks>
    Private Sub btnDelete_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnDelete.Click
        If lstMailboxes.SelectedIndex < 0 Then
            Exit Sub
        End If
        Dim _mbName As String = lstMailboxes.Text
        Dim _asc As New ImapPusher.AppSettingsCollection
        _asc.Settings.Remove(_mbName)
        _asc.Save()
        lstMailboxes.Items.Remove(_mbName)
    End Sub

    Private m_TraceFileLocation As String
    Private m_DisablePopup As Boolean = False

    Private Function GetApplicationDirectory() As String
        Return IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly.GetName.CodeBase.Replace("file:\", ""))
    End Function

    Private m_Id As Integer = -1

    Private Function TryConnect() As Boolean
        Return StartClientThread()
    End Function

    ''' <summary>
    ''' Connect to or start the pusher service.
    ''' </summary>
    ''' <param name="sender"></param>
    ''' <param name="e"></param>
    ''' <remarks></remarks>
    Private Sub btnStart_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnStart.Click
        If Not TryConnect() Then
            Dim _myLoc As String = GetApplicationDirectory()
            Dim _exec As String = IO.Path.Combine(_myLoc, "ImapPusherService.exe")
            m_Id = Shell("""" & _exec & """", AppWinStyle.Hide, False)
            MsgBox("Service started.")
            System.Threading.Thread.Sleep(1000)
            TryConnect()
        Else
            MsgBox("Connected to service.")
        End If
    End Sub

    ''' <summary>
    ''' Try to kill the running service based on the process id.
    ''' </summary>
    ''' <param name="sender"></param>
    ''' <param name="e"></param>
    ''' <remarks></remarks>
    Private Sub btnStop_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnStop.Click
        If m_Id = -1 Then
            MsgBox("Not connected to or aware of the service.")
        End If
        If m_Id <> -1 Then
            Dim _Process As System.Diagnostics.Process = System.Diagnostics.Process.GetProcessById(m_Id)
            If _Process IsNot Nothing Then
                Try
                    _Process.Kill()
                    MsgBox("Killed service process.")
                Catch ex As Exception
                    MsgBox("Can't find process to kill, try a soft reset.")
                End Try
            End If
        End If
    End Sub

    ''' <summary>
    ''' Represents a thread to connect to the pusher service to collect status information.
    ''' </summary>
    ''' <remarks></remarks>
    Private Class ClientThread
        Private m_Client As Net.Sockets.TcpClient

        Public Sub New(ByVal pPort As Integer)
            m_Client = New Net.Sockets.TcpClient()
            m_Client.Connect(New Net.IPEndPoint(Net.IPAddress.Loopback, CInt(pPort)))

        End Sub

        Public Event Health(ByVal pMailbox As String, ByVal pInfo As String)
        Public Event [Error](ByVal pMailbox As String, ByVal pInfo As String)
        Public Event Log(ByVal pInfo As String)
        Public Event PID(ByVal pValue As Integer)

        Protected Sub GetHeadTail(ByVal pString As String, ByRef pHead As String, ByRef pTail As String)
            Dim _firstSep As Integer = pString.IndexOf("@S@")
            pHead = pString.Substring(0, _firstSep)
            pTail = pString.Substring(_firstSep + 3)
        End Sub

        Protected Sub ProcessHealth(ByVal pStatement As String)
            Dim _head As String = Nothing
            Dim _tail As String = Nothing
            GetHeadTail(pStatement, _head, _tail)
            If _head IsNot Nothing AndAlso _head = "HEALTH" Then
                Dim _mbName As String = _tail
                RaiseEvent Health(_mbName, "")
            End If
        End Sub

        Protected Sub ProcessError(ByVal pStatement As String)
            Dim _head As String = Nothing
            Dim _tail As String = Nothing
            GetHeadTail(pStatement, _head, _tail)
            If _head IsNot Nothing AndAlso _head = "ERROR" Then
                Dim _mbName As String
                GetHeadTail(_tail, _head, _tail)
                _mbName = _head
                Dim _info As String = _tail
                RaiseEvent Error(_mbName, _info)
            End If
        End Sub

        Protected Sub ProcessLog(ByVal pStatement As String)
            Dim _head As String = Nothing
            Dim _tail As String = Nothing
            GetHeadTail(pStatement, _head, _tail)
            If _head IsNot Nothing AndAlso _head = "LOG" Then
                RaiseEvent Log(_tail)
            End If
        End Sub

        Protected Sub ProcessPID(ByVal pStatement As String)
            Dim _head As String = Nothing
            Dim _tail As String = Nothing
            GetHeadTail(pStatement, _head, _tail)
            If _head IsNot Nothing AndAlso _head = "PID" Then
                RaiseEvent PID(CInt(_tail))
            End If
        End Sub

        Protected Sub ProcessStatement(ByVal pStatement As String)
            If pStatement.StartsWith("HEALTH") Then
                ProcessHealth(pStatement)
            End If
            If pStatement.StartsWith("ERROR") Then
                ProcessError(pStatement)
            End If
            If pStatement.StartsWith("LOG") Then
                ProcessLog(pStatement)
            End If
            If pStatement.StartsWith("PID") Then
                ProcessPID(pStatement)
            End If
        End Sub

        Protected Sub ProcessLine(ByRef pLine As String)
            Dim _process As String = pLine.Substring(0, pLine.IndexOf(ControlChars.NewLine))
            Dim _rest As String = pLine.Substring(pLine.IndexOf(ControlChars.NewLine) + 2)
            pLine = _rest
            ProcessStatement(_process)
        End Sub

        Public Sub WorkThread()
            Dim _bytes(1023) As Byte

            Dim _ns As Net.Sockets.NetworkStream = m_Client.GetStream
            Dim _i As Integer
            Try
                _i = _ns.Read(_bytes, 0, _bytes.Length)
            Catch ex As Exception
            End Try
            Dim _data As String
            Dim _ongoingData As String = ""
            While _i <> 0
                _data = System.Text.Encoding.ASCII.GetString(_bytes, 0, _i)
                _ongoingData &= _data
                While _ongoingData.IndexOf(ControlChars.NewLine) >= 0
                    ProcessLine(_ongoingData)
                End While
                Try
                    _i = _ns.Read(_bytes, 0, _bytes.Length)
                Catch ex As Exception
                End Try
            End While
        End Sub

        Private m_Thread As System.Threading.Thread
        Public Property Thread() As System.Threading.Thread
            Get
                Return m_Thread
            End Get
            Set(ByVal value As System.Threading.Thread)
                m_Thread = value
            End Set
        End Property

        Public Sub Close()
            m_Client.Close()
        End Sub
    End Class

    Private WithEvents m_ClientThread As ClientThread

    Private Sub ImapPusherServiceManager_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Load
        Dim _asc As New ImapPusher.AppSettingsCollection
        For Each _appSetting As ImapPusher.AppSettings In _asc.Settings.Values
            lstMailboxes.Items.Add(_appSetting.MailboxName)
        Next
        m_TraceFileLocation = _asc.TraceFileLocation
        m_DisablePopup = True
        chkTracing.Checked = _asc.DoTracing
        If _asc.StatusPort <> -1 Then
            nudPort.Value = _asc.StatusPort
        End If
        m_DisablePopup = False

        'StartClientThread()
    End Sub

    Private Function StartClientThread() As Boolean
        Try
            If m_ClientThread IsNot Nothing Then
                m_ClientThread.Thread.Abort()
                m_ClientThread.Close()
                m_ClientThread = Nothing
            End If

            m_ClientThread = New ClientThread(CInt(nudPort.Value))
            Dim _thread As New System.Threading.Thread(New System.Threading.ThreadStart(AddressOf m_ClientThread.WorkThread))
            m_ClientThread.Thread = _thread
            _thread.Start()
            Return True
        Catch ex As Exception
            Return False
        End Try
    End Function

    Private Sub chkTracing_CheckStateChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles chkTracing.CheckStateChanged
        If m_DisablePopup Then Exit Sub
        If chkTracing.Checked Then
            Dim _d As New System.Windows.Forms.SaveFileDialog()
            If m_TraceFileLocation IsNot Nothing Then
                _d.FileName = m_TraceFileLocation
            End If
            _d.ShowDialog()
            m_TraceFileLocation = _d.FileName
        End If
        Dim _asc As New ImapPusher.AppSettingsCollection
        _asc.TraceFileLocation = m_TraceFileLocation
        _asc.Save()
    End Sub

    Private Sub nudPort_ValueChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles nudPort.ValueChanged
        If m_DisablePopup Then Exit Sub
        StartClientThread()
        Dim _asc As New ImapPusher.AppSettingsCollection
        _asc.StatusPort = CInt(nudPort.Value)
        _asc.Save()
    End Sub

    Private m_Lock As New Object
    Private m_Status As New Generic.Dictionary(Of String, StatusInfo)
    Private m_Log As New Generic.Queue(Of String)

    Private Sub m_ServerThread_Error(ByVal pMailbox As String, ByVal pInfo As String) Handles m_ClientThread.Error
        SyncLock m_Lock
            If Not m_Status.ContainsKey(pMailbox) Then
                m_Status.Add(pMailbox, New StatusInfo)
            End If
            m_Status(pMailbox).AddError(pInfo)
        End SyncLock
    End Sub

    Private Sub m_ServerThread_Health(ByVal pMailbox As String, ByVal pInfo As String) Handles m_ClientThread.Health
        SyncLock m_Lock
            If Not m_Status.ContainsKey(pMailbox) Then
                m_Status.Add(pMailbox, New StatusInfo)
            End If
            m_Status(pMailbox).AddHealthy()
        End SyncLock
    End Sub

    Public Sub AddLog(ByVal pLog As String)
        SyncLock m_Lock
            m_Log.Enqueue(pLog)
            If m_Log.Count > 200 Then
                m_Log.Dequeue()
            End If
        End SyncLock
    End Sub

    Private Sub m_ServerThread_Log(ByVal pInfo As String) Handles m_ClientThread.Log
        SyncLock m_Lock
            AddLog(pInfo)
        End SyncLock
    End Sub

    Private Sub m_ServerThread_PID(ByVal pValue As Integer) Handles m_ClientThread.PID
        SyncLock m_Lock
            If m_Id = -1 Then
                AddLog("PID changed to " & pValue)
            End If
            m_Id = pValue
        End SyncLock
    End Sub

    Private Sub btnStatus_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnStatus.Click
        Dim _si As StatusInfo = Nothing
        If lstMailboxes.SelectedIndex < 0 Then
            Exit Sub
        End If
        Dim _mbName As String = lstMailboxes.Text

        SyncLock m_Lock
            If m_Status.ContainsKey(_mbName) Then
                _si = m_Status(_mbName).GetBufferedCopy
            End If
        End SyncLock
        If _si IsNot Nothing Then
            Dim _statusDisplay As New StatusDisplay
            _statusDisplay.StatusInfo = _si
            _statusDisplay.PID = m_Id
            _statusDisplay.ShowDialog()
        End If
    End Sub

    Private Sub btnLog_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnLog.Click
        SyncLock m_Lock
            Dim _str As New Generic.List(Of String)
            For Each _entry In m_Log
                _str.Add(_entry)
            Next
            Dim _dispLog As New DisplayLog
            _dispLog.Log = _str
            _dispLog.ShowDialog()
        End SyncLock
    End Sub

    <DllImport("coredll.dll")> _
    Private Shared Sub SHCreateShortcut(ByVal shortcut As String, ByVal target As String)
    End Sub
End Class

Public Class StatusInfo
    Private m_NumErrors As Integer
    Private m_LastError As String
    Private m_LastHealthy As DateTime
    Private m_Lock As New Object
    
    Public Property NumErrors() As Integer
        Get
            Return m_NumErrors
        End Get
        Set(ByVal value As Integer)
            m_NumErrors = value
        End Set
    End Property
    Public Property LastError() As String
        Get
            Return m_LastError
        End Get
        Set(ByVal value As String)
            m_LastError = value
        End Set
    End Property
    Public Property LastHealthy() As DateTime
        Get
            Return m_LastHealthy
        End Get
        Set(ByVal value As DateTime)
            m_LastHealthy = value
        End Set
    End Property
    
    Public Sub AddError(ByVal pError As String)
        SyncLock m_Lock
            m_NumErrors += 1
            m_LastError = pError
        End SyncLock
    End Sub

    

    Public Sub AddHealthy()
        SyncLock m_Lock
            m_LastHealthy = DateTime.Now
        End SyncLock
    End Sub

    Public Function GetBufferedCopy() As StatusInfo
        SyncLock m_Lock
            Dim _si As New StatusInfo
            _si.m_NumErrors = m_NumErrors
            _si.m_LastError = m_LastError
            _si.m_LastHealthy = m_LastHealthy
            Return _si
        End SyncLock
    End Function
End Class